<template>
  <div>
    <el-upload class="upload" ref="upload"
      v-model:file-list="fileList"
      v-bind="uploadOptionsVariable.attributes"
      :on-change="uploadChange"
      :http-request="uploadRequest"
      :on-success="uploadSuccess"
      :on-error="uploadError"
      :on-preview="filePreview"
      :on-remove="fileRemove"
      :on-exceed="fileExceed"
    >
      <slot>
        <el-button type="primary">点击上传</el-button>
      </slot>

      <template #tip v-if="uploadOptionsVariable.tip">
        <div class="upload__tip">{{ uploadOptionsVariable.tip }}</div>
      </template>
    </el-upload>

    <el-dialog v-model="cropDialogVisible" title="图片剪裁" :before-close="cancelCropper" @open="initCropper">
      <div class="dialog">
        <div class="dialog__left">
          <img ref="cropImage" :src="currentCropInfo.url"/>
        </div>

        <div class="dialog__right">
          <!-- 预览 -->
          <div class="dialog__preview">
            <div class="dialog__preview__text">预览</div>
            <div class="dialog__preview__image" ref="cropPreview"
              :style="{aspectRatio:cropperOptionsVariable.options.aspectRatio}"
            />
          </div>

          <!-- 操作 -->
          <div class="dialog__operation">
            <div class="dialog__operation__item" @click="cropperInstance.zoom(0.1)">
              <el-icon>
                <ZoomIn/>
              </el-icon>
              放大
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.zoom(-0.1)">
              <el-icon>
                <ZoomOut/>
              </el-icon>
              缩小
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.rotate(90)">
              <el-icon>
                <RefreshRight/>
              </el-icon>
              顺时针旋转
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.rotate(-90)">
              <el-icon>
                <RefreshLeft/>
              </el-icon>
              逆时针旋转
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.scale(-1, 1)">
              <ArkIconFont cls="icon-flip"/>
              水平翻转
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.scale(1, -1)">
              <ArkIconFont cls="icon-flip" class="rotate-90"/>
              垂直翻转
            </div>
            <div class="dialog__operation__item" @click="cropperInstance.reset()">
              <el-icon>
                <Refresh/>
              </el-icon>
              重置
            </div>
          </div>
        </div>
      </div>

      <template #footer>
        <el-button @click="cancelCropper">取 消</el-button>

        <el-button type="primary"
          @click="confirmCropper"
          :loading="confirmLoading"
        >
          确 定
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import {defineComponent} from 'vue';
import _ from 'lodash';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import ArkIconFont from '@/components/ark-component-v1/ark-iconfont/ArkIconFont.vue';
import {ElMessage} from 'element-plus';
import ArkUploadConstant from './ArkUploadConstant.js';

export default defineComponent({
  name: 'ArkUpload',
  components: {ArkIconFont},
  emits: ['update:modelValue'],
  props: {
    /**
     * 上传配置对象
     *  - attributes：表单v-bind对象
     *  - accept：接受上传的文件类型
     *  - maxFileSize：接受上传的文件最大大小
     *  - tip：提示说明文字内容
     *  - contextPath：上下文路径，用于拼接url预览文件
     *  - request：向后端发送请求上传文件的请求函数，该函数的返回值的结果需要注意
     *   - 返回值结构
     *    - code：响应状态码
     *    - msg：响应状态描述
     *    - data：响应数据
     *     - id：存于数据库文件表的唯一id
     *     - relativePath：上传后的文件相对路径
     *     - originalName：原始文件名，包含扩展名
     *     - extName：文件扩展名
     *     - mimeType：文件类型
     *     - fileSize：文件大小
     *  - error：发生错误的函数，参数为错误描述
     *  - resDataKey：返回数据结构键名配置
     */
    uploadOptions: {
      type: Object,
    },
    /**
     * 剪裁配置对象
     *  - disabled：是否禁用图片剪裁
     *  - width：对话框宽度
     *  - options：初始化cropper的配置对象
     *  - success：剪裁对话框点击确定后上传成功后的函数
     */
    cropperOptions: {
      type: Object,
    },
    /**
     * 文件列表，配合v-model实现双向绑定
     */
    modelValue: {
      type: Array,
    }
  },
  data() {
    return {
      uploadOptionsVariable: {
        attributes: {
          action: '/',
          multiple: false,
          'auto-upload': false,
        },
        accept: null,
        maxFileSize: null,
        request: () => { },
        error: (msg) => {
          console.error(msg);
        },
        resDataKey: 'data',
      },
      cropperOptionsVariable: {
        disabled: false,
        width: '80%',
        options: {
          viewMode: 2,
          aspectRatio: 1,
        },
        success: () => {},
      },
      fileList: [],
      cropDialogVisible: false,
      cropperInstance: {},
      currentCropInfo: {
        uid: '',
        url: '',
      },
      confirmLoading: false,
    };
  },
  computed: {
    wrapFileList: {
      get() {
        const modelValue = this.modelValue;
        if (!modelValue || modelValue.length === 0) {
          return [];
        }
        return modelValue;
      },
      set(value) {
        const contextPath = this.uploadOptionsVariable.contextPath;
        this.fileList = value.map(item => {
          item.name = item.originalName;
          item.url = !contextPath || item.relativePath?.startsWith(contextPath) ? item.relativePath : contextPath + item.relativePath;
          return item;
        });
        this.$emit('update:modelValue', value);
      },
    },
    dialogWidth() {
      let width = this.cropperOptionsVariable.width;
      if (!Number.isNaN(Number(width))) {
        width = width + 'px';
      }
      return width;
    }
  },
  created() {
    this.initAllOptionsVariable();

    const contextPath = this.uploadOptionsVariable.contextPath;
    this.fileList = this.modelValue.map(item => {
      item.name = item.originalName;
      item.url = !contextPath || item.relativePath?.startsWith(contextPath) ? item.relativePath : contextPath + item.relativePath;
      return item;
    });
  },
  methods: {
    initAllOptionsVariable() {
      // 根据传入uploadOptions初始化uploadOptionsVariable
      const uploadOptions = _.cloneDeep(this.uploadOptions);
      const uploadOptionsVariable = _.cloneDeep(this.uploadOptionsVariable);
      this.uploadOptionsVariable = _.defaultsDeep(uploadOptions, uploadOptionsVariable);

      if (this.uploadOptionsVariable.accept === ArkUploadConstant.type.image) {
        this.uploadOptionsVariable.attributes.accept = 'image/*';
      }

      // 根据传入cropperOptions初始化cropperOptionsVariable
      const cropperOptions = _.cloneDeep(this.cropperOptions);
      const cropperOptionsVariable = _.cloneDeep(this.cropperOptionsVariable);
      this.cropperOptionsVariable = _.defaultsDeep(cropperOptions, cropperOptionsVariable);
    },
    uploadChange(elFile) {
      /**
       * elFile
       *  - name：文件名
       *  - percentage：上传进度
       *  - status：上传状态
       *  - size：文件大小
       *  - uid
       *  - raw：File
       */

      if (elFile.status !== 'ready') return;

      const el = this.$refs.upload;

      // 文件格式检查
      if (!this.passFileTypeCheck(elFile.raw)) {
        el.handleRemove(elFile);
        ElMessage.error('文件格式不支持');
        return;
      }

      // 大小检查
      if (!this.passFileSizeCheck(elFile.raw)) {
        el.handleRemove(elFile);
        ElMessage.error('文件大小超过限制');
        return;
      }

      // 非图片剪裁
      if (!this.passFileImageCrop(elFile.raw)) {
        el.submit();
        return;
      }

      /**
       * URL.createObjectURL创建的链接类似于blob:http://localhost:6006/f21156d3-bcd9-4b9a-bf1c-c9eb0c354ad3，当不在需要时使用URL.revokeObjectURL()释放
       */
      this.currentCropInfo = {
        uid: elFile.uid,
        url: URL.createObjectURL(elFile.raw),
      };
      this.cropDialogVisible = true;
    },
    passFileTypeCheck(file) {
      /**
       * file
       *  - uid
       *  - lastModified：最后修改时间，例如1647251673409
       *  - lastModifiedDate：最后修改时间，例如Mon Oct 09 2023 14:02:03 GMT+0800 (中国标准时间)
       *  - name：文件名，包括后缀
       *  - size：文件大小
       *  - type：文件类型
       *  - webkitRelativePath
       */

      const acceptOption = this.uploadOptionsVariable.accept;
      const acceptAttribute = this.uploadOptionsVariable.attributes.accept;

      // 文件无限制
      if (!acceptOption && !acceptAttribute) return true;

      // this.uploadOptionsVariable.accept限制
      const fileType = file.type;
      if (acceptOption && acceptOption === ArkUploadConstant.type.image) {
        return fileType.startsWith('image');
      }

      // this.uploadOptionsVariable.attributes.accept限制
      if (acceptAttribute) {
        acceptAttribute.split(',').forEach(item => {
          const acceptItem = item.trim().toLowerCase();
          if (acceptItem.startsWith('.')) { // 后缀名判断规则
            const splitFileName = file.name.split('.');
            return `.${splitFileName[splitFileName.length - 1].toLowerCase()}` === acceptItem;
          } else { // mime判断规则
            const fileTypePrefix = fileType.split('/')[0];
            return acceptItem.includes(fileTypePrefix);
          }
        });
      }

      return false;
    },
    passFileSizeCheck(file) {
      /**
       * file
       *  - uid
       *  - lastModified：最后修改时间，例如1647251673409
       *  - lastModifiedDate：最后修改时间，例如Mon Oct 09 2023 14:02:03 GMT+0800 (中国标准时间)
       *  - name：文件名，包括后缀
       *  - size：文件大小
       *  - type：文件类型
       *  - webkitRelativePath
       */

      const maxFileSize = this.uploadOptionsVariable.maxFileSize;

      // 大小无限制
      if (!maxFileSize) return true;

      return file.size <= maxFileSize;
    },
    passFileImageCrop(file) {
      /**
       * file
       *  - uid
       *  - lastModified：最后修改时间，例如1647251673409
       *  - lastModifiedDate：最后修改时间，例如Mon Oct 09 2023 14:02:03 GMT+0800 (中国标准时间)
       *  - name：文件名，包括后缀
       *  - size：文件大小
       *  - type：文件类型
       *  - webkitRelativePath
       */
      return file.type.startsWith('image') && !this.cropperOptionsVariable.disabled;
    },
    initCropper() {
      // 剪裁实例已被创建
      if (Object.keys(this.cropperInstance).length !== 0) {
        this.cropperInstance.replace(this.currentCropInfo.url);
        return;
      }

      this.cropperInstance = new Cropper(this.$refs.cropImage, {
        ...this.cropperOptionsVariable.options,
        preview: this.$refs.cropPreview,
      });
    },
    cancelCropper() {
      // 移除文件
      const uid = this.currentCropInfo.uid;
      this.fileList = this.fileList.filter(item => {
        return item.uid !== uid;
      });
      this.wrapFileList = this.wrapFileList.filter(item => {
        return item.uid !== uid;
      });

      // 销毁剪裁
      URL.revokeObjectURL(this.currentCropInfo.url);

      // 关闭对话框
      this.cropDialogVisible = false;
      this.confirmLoading = false;
    },
    confirmCropper() {
      this.confirmLoading = true;

      this.cropperInstance.getCroppedCanvas().toBlob((blob) => {
        /**
         * blob: {size, type:'image/png'}
         */

        const index = this.fileList.findIndex(item => {
          return item.uid === this.currentCropInfo.uid;
        });

        if (index > -1) {
          const elFile = this.fileList[index];
          elFile.size = blob.size;
          elFile.type = blob.type;

          elFile.raw = new File([blob], elFile.name);
          elFile.raw.uid = elFile.uid;

          this.$refs.upload.submit();
        }

        /**
         * 本来应该等到上传完后再关闭对话框，但不关闭还有许多问题需要处理，所以此处直接关闭对话框
         */
        // 销毁剪裁
        URL.revokeObjectURL(this.currentCropInfo.url);

        // 关闭对话框
        this.cropDialogVisible = false;
        this.confirmLoading = false;
      });
    },
    uploadRequest(elFile) {
      /**
       * elFile
       *  - action：上传地址，例如"/"
       *  - headers：{}
       *  - method："post"
       *  - data：{}
       *  - filename："file"
       *  - file：File，与后端MultipartFile对象匹配
       *  - withCredentials：false
       *  - onProgress：(evt)=>{...}
       *  - onSuccess：(res)=>{...}
       *  - onError：(err)=>{...}
       */

      Promise.resolve(this.uploadOptionsVariable.request(elFile)).then(data => {
        // 获取上传请求的结果
        let uploadData = {};
        const resDataKey = this.uploadOptionsVariable.resDataKey;
        if (data && resDataKey) {
          uploadData = data[resDataKey];
        } else { // 未设置dataKey
          uploadData = data;
        }

        // 结果数据为空
        const el = this.$refs.upload;
        if (!uploadData) {
          this.uploadOptionsVariable.error('请求函数获取的数据为空');
          el.handleRemove(elFile);
          return;
        }

        // 重新包装结果并记录
        const contextPath = this.uploadOptionsVariable.contextPath;
        const wrapUploadData = {
          ...uploadData,
          uid: elFile.file.uid,
          type: elFile.file.type,
          name: uploadData.originalName,
          url: !contextPath || uploadData.relativePath?.startsWith(contextPath) ? uploadData.relativePath : contextPath + uploadData.relativePath,
        };
        this.wrapFileList = this.wrapFileList.concat([wrapUploadData]);
        elFile.onSuccess();
      }).catch(error => {
        elFile.onError(error);
      });
    },
    uploadSuccess(response, elFile, fileList) {
      /**
       * elFile
       *  - name：文件名
       *  - percentage：上传进度
       *  - response
       *  - status：上传状态
       *  - size：文件大小
       *  - uid
       *  - raw：File
       */

        // 图片剪裁
      const file = elFile.raw.type === '' ? elFile : elFile.raw;
      if (!this.passFileImageCrop(file)) {
        return;
      }

      // 确认函数
      if (!this.cropperOptionsVariable.success || typeof this.cropperOptionsVariable.success !== 'function') {
        return;
      }

      this.cropperOptionsVariable.success();
    },
    uploadError(error, elFile, fileList) {
      this.confirmLoading = false;
    },
    filePreview(file) {
      const index = this.wrapFileList.findIndex(item => {
        return item.uid === file.uid;
      });
      const url = this.wrapFileList[index].url;
      window.open(file.url, '_blank');
    },
    fileRemove(elFile) {
      /**
       * elFile
       *  - name：文件名
       *  - percentage：上传进度
       *  - response
       *  - status：上传状态
       *  - size：文件大小
       *  - uid
       *  - raw：File
       */

      this.wrapFileList = this.wrapFileList.filter(item => {
        return item.uid !== elFile.uid;
      });
    },
    fileExceed() {
      ElMessage.error(`最多只能上传${this.uploadOptionsVariable.attributes.limit}个文件`);
    },

    // 供外部调用，内部未使用
    clearFileList() {
      this.$refs.upload.clearFiles();
      this.wrapFileList = [];
    },
  },
});
</script>

<style scoped>
.upload {
  .upload__tip {
    @apply mt-1.5 text-sm text-[#606266];
  }
}

:deep(.el-overlay) {
  .el-dialog {
    min-width: v-bind(dialogWidth);
    @apply rounded-xl;
    /* min-width: 1000px; */

    .dialog {
      @apply flex gap-x-9;

      .dialog__left {
        @apply w-[60%] h-[350px] ;
      }

      .dialog__right {
        .dialog__preview {
          .dialog__preview__text {
            @apply mb-6 text-base font-semibold;
          }

          .dialog__preview__image {
            @apply max-h-[200px] m-auto overflow-hidden;
            @apply border border-solid border-[#BFBFBFFF];
          }
        }

        .dialog__operation {
          @apply pt-6 flex flex-wrap gap-x-4 gap-y-3;

          .dialog__operation__item {
            @apply flex justify-center items-center gap-x-1 cursor-pointer;
          }
        }
      }
    }
  }
}
</style>
